Skip to content

Implement Mahony filter for attitude estimation - #142

Open
bjornhbengtsson wants to merge 14 commits into
feature/gravity-compensated-accelerationfrom
feature/mahony-attitude-filter
Open

Implement Mahony filter for attitude estimation#142
bjornhbengtsson wants to merge 14 commits into
feature/gravity-compensated-accelerationfrom
feature/mahony-attitude-filter

Conversation

@bjornhbengtsson

@bjornhbengtsson bjornhbengtsson commented Jul 30, 2026

Copy link
Copy Markdown

Description

Implements the Mahony attitude filter for quaternion based attitude estimation.

The filter maintains a world-to-body attitude quaternion using body frame gyroscope propagation with proportional and integral accelerometer feedback.

Made with assistance of ChatGPT 5.6.

Changes include:

  • Added a standalone Mahony attitude filter module
  • Added gyroscope based quaternion propagation
  • Added proportional accelerometer correction
  • Added integral gyro bias correction
  • Added per axis integral anti windup limits
  • Added accelerometer magnitude validity gating
  • Added support for externally enabling or disabling accelerometer correction
  • Added gyro only fallback when accelerometer samples are disabled or invalid
  • Added input validation and quaternion normalization
  • Integrated the Mahony attitude estimate into sensor state estimation
  • Added gain selection rationale and implementation comments
  • Added diagnostic Euler angle and quaternion output tests
  • Added comprehensive Mahony unit tests
  • Converted the stored attitude and quaternion rotation helpers to a body-to-world convention

Issue Link

Depends on #138
#138

Closes #141
#141

Testing

  • Unit tests added

Mahony unit test results:
image
-------------Test Complete--------------

Passes: 140
Fails: 0
Result: PASS

Math test results:

-------------Test Complete--------------

Passes: 26
Fails: 0
Result: PASS

Other

Accelerometer correction is only applied when:

  • the caller enables accelerometer feedback
  • the sample contains finite values
  • the measured acceleration magnitude lies within the configured near 1 g validity range

When accelerometer feedback is disabled or rejected, the filter continues using gyroscope only propagation.

The current accelerometer thresholds, proportional gain, integral gain, and integral limit are initial software values. Final tuning should be performed using hardware characterization, vibration testing, and flight data.

Deferred work includes:

  • Final flight state accelerometer correction policy
  • Hardware and flight data gain tuning
  • Additional correction validity telemetry
  • Calibrated magnetometer feedback for yaw correction

Reviewer Checklist

Standards

  • Follows FCF Architectural Standards
  • Follows SDR Coding Standards
  • Code complexity/function Size is minimized
  • Code is testable
  • Code is readable and commented properly
  • License terms are respected

Error Handling

  • Potentially unsafe functions return a status code
  • Error returns properly handled

Memory

  • Stack allocated memory is scoped correctly
  • Heap allocated memory is avoided
  • Globally allocated memory is minimized except when necessary
  • Pointers are used correctly
  • Concurrency has been considered

Performance

  • Rate limiters are respected
  • Busy waiting is avoided
  • "Delay" calls are not used in performance sensitive code

@bjornhbengtsson bjornhbengtsson changed the title Implement Mahony filter for attitude estimations Implement Mahony filter for attitude estimation Jul 30, 2026
@NArmistead

Copy link
Copy Markdown
Contributor

Yeeeeeees 🔥🔥🔥🔥🔥🔥will review soon

@NArmistead

Copy link
Copy Markdown
Contributor

2600 line test 💀 wdym

@bjornhbengtsson

Copy link
Copy Markdown
Author

Yeah I think its nuts too 🐿️

@NArmistead

Copy link
Copy Markdown
Contributor

You'll need a PR in FCF to add mahony.c and the header to the Makefile

@bjornhbengtsson

Copy link
Copy Markdown
Author

@bjornhbengtsson

Copy link
Copy Markdown
Author

For reference, commit 4fe0097 contains the world to body Mahony quaternion convention. Commit 13ce68c reverts that change and restores the body to world convention. We tested both versions on the flight computer hardware and found that the body to world implementation produced the expected gyro integration and attitude behavior, so that is the version being kept for this PR. If the world to body implementation needs to be revisited later, it can be checked out directly at 4fe0097.

@NArmistead NArmistead left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main changes I'm requesting here are:

  1. An error status enum for the mahony filter. This is consistent with our standards and will make debugging a little easier since we know which error we're getting.
  2. Start the filter before the prelaunch terminal so it's active if we're doing dashboard dumps.

I also think it would be more readable if you consolidated some of these functions into one line. I know we don't have a style guide but you can look around our code for some examples (should_log_next_frame is a particularly bad offender for a long line, but probably most readable that way)

All that being said, through all the testing I've done at home this filter does seem to be working and it's great to finally have a step into some better sensor fusion algorithms. This PR should be merged with #138, but I see you already put it in a stack.

Comment thread mahony/mahony.c
Comment on lines +210 to +230
if ( filter == NULL )
{
return false;
}

if ( !mahony_quat_is_finite(initial_attitude) )
{
return false;
}

if ( !isfinite(proportional_gain) ||
!isfinite(integral_gain) )
{
return false;
}

if ( proportional_gain < 0.0f ||
integral_gain < 0.0f )
{
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cr: add a status enum for specific error returns

Comment thread sensor/sensor.c Outdated

sensor_reset_velo();

(void)mahony_init

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cr: throw an error if this fails.

sensor init happens during calibration so we can still throw fail-fast errors since this isn't the flight loop yet

Comment thread sensor/sensor.c
imu_velo_tick = get_us_tick();

sensor_reset_velo();
QUAT identity =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: define the identity quat as a constant, probably somewhere in math_sdr.h

Comment thread sensor/sensor.c Outdated

sensor_reset_velo();

(void)mahony_init

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another cr here: we need to start the filter before sensor calibration (which currently calls this function) because it's needed for dashboard dumps during prelaunch

you could probably get away with just running this once in main during startup and then once again during calibration

Comment thread sensor/sensor.c
Comment on lines +489 to +491
delta_time_s =
(float)imu_tdelta /
(float)MICROSEC_PER_SEC;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is short enough that it fits on one line

Comment thread mahony/mahony.c
Comment on lines +366 to +370
accel_valid =
mahony_vector_is_finite(accel_body) &&
isfinite(accel_magnitude) &&
accel_magnitude >= MAHONY_ACCEL_MIN_MAGNITUDE &&
accel_magnitude <= MAHONY_ACCEL_MAX_MAGNITUDE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think our usual convention is to put the logical operators at the start of each new line (sorry this is hard to format here
accel_valid = mahony_vector_is_finite(accel_body)
&& isfinite(accel_magnitude)
&& etc..

Comment thread sensor/sensor.c Outdated
use_accel =
get_fc_state() <= FC_STATE_LAUNCH_DETECT;

(void)mahony_update_imu

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cr: we probably want to check the status here too. in this case, we could at least use a debug_assert to catch if anything weird happens. worst case we could also reinitialize the filter if something completely breaks

Comment thread test/mahony/Makefile
@@ -0,0 +1,106 @@
################################################################
#
# math_sdr unit tests (based on gcc)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: mahony?

Comment thread test/mahony/test_mahony.c Outdated

for ( index = 0; index < 2000; index++ )
{
TEST_ASSERT_TRUE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you don't want to do 2000 assertions in this loop and you make a status enum where success is 0 in the enum (our convention), and there are no other acceptable states (i.e. the rest of the returns are some form of error, which is the case here), you could |= the status return each time so that you only need to assert once at the end.

that would also make the results file a little more navigable and prevent you from having 20000 (!!) asserts

Comment thread test/mahony/test_mahony.c

@NArmistead NArmistead Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a really long file to go through every test, so im gonna leave this as is for now and come back to it once you make changes to everything else. these are some pretty nice and thorough tests though and good to verify with determinate tests that the filter does what it should

@NArmistead

Copy link
Copy Markdown
Contributor

Another thing I noticed is that enabling integral correction with the accelerometer will affect the heading (global z axis) and create drift, which it shouldn't do because acceleration doesn't tell us anything about that. Presumably mag fusion would fix that?

@bjornhbengtsson

Copy link
Copy Markdown
Author

new mahony unit test result

-------------Test Complete--------------
----------------------------------------
Passes: 140
Fails:  0
Result: PASS
----------------------------------------

@bjornhbengtsson

Copy link
Copy Markdown
Author

I addressed the remaining Mahony review comments:

Added MAHONY_STATUS enum return codes and updated the related tests.
sensor_init() now handles mahony_init() failures.
sensor_body_state() now checks mahony_update_imu() status.
Fixed the release-build -Werror issue caused by debug_assert() compiling out.
Updated the parent firmware to initialize Mahony before the prelaunch terminal while keeping the existing post-calibration reinitialization.
Added the required sensor_init() stub to the fsm_appa unit tests.

Validation: Mahony tests pass, math_sdr tests pass, the fsm_appa test passes, and both debug and release Rev2 firmware builds pass.

@bjornhbengtsson

Copy link
Copy Markdown
Author

I left the style changes to you, seems more effective that way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants